Numbering Child Records

Description

Many applications create a set with a parent table in a one-to-many relationship with a child table. Field Rules auto-numbering can assign unique ID numbers to the records in the parent table. The problem of numbering the child table records is cannot be solved with Field Rules, particularly if you want to reset the child record counter to 1 for each new parent record. For example, the solution might provide the following record numbers:

Parent Record Number

Child Record Numbers

1

01 to 04

2

01 to 02

3

01 to 07

4

01

One additional requirements could be that the deletion of a child record would cause the remaining child records to be renumbered. Assume there are two tables: invoice and line_items with the following structures.

images/dbtable3.png

The two tables are linked one-to-many on the Invoice_ID field.

A script placed under the OnSaveRecord event of the line_items table numbers new records. The same script placed under the OnDeleteRecord event renumbers line items after one is deleted.

dim head as P
dim items as P
dim line_number as N
line_number = 0

The TABLE.GET() method returns a pointer to an open table (in this case the parent table in the set). This line verifies that the line_items child table is being opened in a set with parent table. This is very important. If the line_items table is not opened as a child table, then there is no meaningful way to assign consecutive line numbers to the records in the same invoice. If the table is opened by itself, then the script passes program flow to the error trap at not_in_set and does not renumber the line item records.

on error goto not_in_set
head = table.get("invoice")

Once we have verified that we are in the right set, we sequentially fetch through the records in line_items. We use <TBL>.FETCH_FIND() and <TBL>.FETCH_LAST() in a WHILE ... END WHILE loop. The set structure guarantees that active range of the line_items records is limited to the current header number.

on error goto 0
items = table.get("line_items")
items.fetch_first()
while .not. items.fetch_eof()
    line_number = line_number + 1
    items.change_begin()
    items.line_number = padl(ltrim(str(line_number, 2, 0)), 2, "0")
    items.change_end(.t.)
    items.fetch_next()
end while

After fetching each record in line_items, we have to update its line_number field. That is done with the <TBL>.CHANGE_BEGIN() and <TBL>.CHANGE_END() methods. Finally, after updating the records in the browse, we issue a PARENTFORM.RESYNCH() to make sure the form reflects the changes made to the underlying table.

parentform.resynch()
end
not_in_set:
on error goto 0
end

You can use this technique any time you need to number child records consecutively. Examples would include invoices for customers, clinic visits for patients, monthly deposits or withdrawals in an account - the number of such applications is unlimited.

Multiuser considerations

Under rare circumstances, it is possible for a multi-user record-locking conflict to arise. If two users are simultaneously adding or deleting line items on a single invoice, then conceivably one of the OnSaveRecord or OnDeleteRecord scripts could fail to obtain the record lock it needs to initiate a .CHANGE_BEGIN(). This would be a rare circumstance indeed, but of course it is just such rare circumstances that are behind most spectacular software failures.

We can check for multi-user conflicts by placing another on error goto statement just before the .CHANGE_BEGIN(). as in all error traps, we have to decide in advance what we will do if we encounter an error. In this case, we will just delay one second and try again. If the record is still locked and we can't change it, we will delay a second and then a third time. If we continue to fail after 3 tries, we will abort the script with an informative message to the operator. Here is our script with multi-user error trapping added:

dim head as P
dim items as P
dim line_number as N
dim tries as N
tries = 0
line_number = 0
on error goto not_in_set
head = table.get("invoice")
on error goto 0
items = table.get("line_items")
items.fetch_first()
while .not. items.fetch_eof()
    line_number=line_number + 1
    on error goto record_locked
    items.change_begin()
    on error goto 0
    items.line_number=padl(ltrim(str(line_number, 2, 0)), 2, "0")
    items.change_end(.t.)
    items.fetch_next()
    tries = 0
end while
parentform.resynch()
end
not_in_set:
on error goto 0
end
record_locked:
on error goto 0
if (tries < 3) then
    tries = tries + 1
    sleep(1)
    resume 0
else
    ui_msg_box("System error", "Unable to lock and update line numbers!")
    end
end if

Thanks To

Dr. Peter Wayne

Limitations

Desktop applications only. Not available in Community Edition.

See Also